#include "control.h"
#include "IO.h"
#define CMD_TRAIN "train"
#define CMD_CLASS "class"

Control::Control(){}

int Split(std::vector<std::string>& vecteur, std::string chaine, char separateur)
{
    vecteur.clear();
    std::string::size_type stTemp = chaine.find(separateur);
    while(stTemp != std::string::npos)
    {
	vecteur.push_back(chaine.substr(0, stTemp));
	chaine = chaine.substr(stTemp + 1);
	stTemp = chaine.find(separateur);
    }
    vecteur.push_back(chaine);
    return vecteur.size();
}

void Control::processCommand(int argc, char* argv[])
{
    std::string train_path, test_path;
    std::string descriptor_train, descriptor_test;
    std::string res_folder;
    int nb_block = 10;
    std::string cmd;
    if(argc < 7)
        return;
    cmd = std::string(argv[1]);
    train_path = std::string(argv[2]);
    test_path = std::string(argv[3]);
    descriptor_train = std::string(argv[4]);
    descriptor_test = std::string(argv[5]);
    if(cmd.compare(CMD_TRAIN) == 0)
    {
	nb_block = atoi(argv[6]);
	this->trainning(train_path, test_path,
			descriptor_train, descriptor_test, nb_block);
    }
    if(cmd.compare(CMD_CLASS) == 0)
    {
	res_folder = std::string(argv[6]);
	this->classification(train_path, test_path,
			     descriptor_train, descriptor_test, res_folder);
    }
}

Mat Control::norm_0_255(InputArray _src) {
    Mat src = _src.getMat();
    // Create and return normalized image:
    Mat dst;
    switch(src.channels()) {
    case 1:
        cv::normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC1);
        break;
    case 3:
        cv::normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC3);
        break;
    default:
        src.copyTo(dst);
        break;
    }
    return dst;
}

void match(std::vector<Channel> &channels, std::vector<int> types,
	       vector<string> names)
{
    int N1 = channels.size();
    int N2 = types.size();
    int N3 = names.size();
    if(N1 != N2 || N1 != N3 || N3 != N2)
        return;
    for (int i = 0; i < N1; ++i)
    {
        channels[i].setType(types[i]);
	channels[i].setName(names[i]);
    }
}

void Control::read_csv(const string& filename, vector<Channel>& images,
		       vector<int>& types, vector<string>& names,
		       char separator)
{
    std::ifstream file(filename.c_str(), ifstream::in);
    if (!file) {
        string error_message = "No valid input file was given, please check the given filename.";
        CV_Error(CV_StsBadArg, error_message);
    }
    string line, path, classlabel;
    while (getline(file, line)) {
        stringstream liness(line);
        getline(liness, path, separator);
        getline(liness, classlabel);
        if(!path.empty() && !classlabel.empty()) {
	    int type = atoi(classlabel.c_str());
	    Channel image(imread(path, 0));
	    images.push_back(image);
	    types.push_back(type);

	    std::vector<std::string> dirs;
	    Split(dirs, path, '/');
	    int size = dirs.size();
	    std::string name = dirs[size - 1];
	    names.push_back(name);
        }
    }
}

void Control::trainning(std::string csv_train, std::string csv_test,
			std::string descriptor_train, std::string descriptor_test,
			int nb_block)
{
    vector<Channel> images_train;
    vector<int> types_train;
    vector<string> names_train;
    vector<Channel> images_test;
    vector<int> types_test;
    vector<string> names_test;
    // Read in the data. This can fail if no valid
    // input filename is given.
    try {
        read_csv(csv_train, images_train, types_train, names_train);
	read_csv(csv_test, images_test, types_test, names_test);
    } catch (cv::Exception& e) {
        cerr << "Error opening file \""
	     << csv_train <<"or "  <<csv_test
	     << "\". Reason: " << e.msg << endl;
        // nothing more we can do
        exit(1);
    }
    // Quit if there are not enough images for this demo.
    if(images_train.size() <= 1 || images_test.size() <= 1) {
        string error_message = "This demo needs at least 2 images to work. Please add more images to your data set!";
        CV_Error(CV_StsError, error_message);
    }
    match(images_train, types_train, names_train);
    match(images_test, types_test, names_test);
    int size_train = images_train.size();
    int size_test = images_test.size();
    ofstream ofs_train(descriptor_train.c_str());
    ofstream ofs_test(descriptor_test.c_str());
    for (int i = 0; i < size_train; ++i)
    {
	//std::cout<<types_train[i] <<" " <<names_train[i]<<endl;
	//std::cout<<images_train[i].getType() <<" " <<images_train[i].getName()<<endl;
	images_train[i].writeDescriptor(ofs_train,
					images_train[i].getDescriptor(nb_block));
    }
    for (int i = 0; i < size_test; ++i)
    {
	//std::cout<<images_test[i].getType() <<" " <<images_test[i].getName() <<endl;
	images_test[i].writeDescriptor(ofs_test,
				       images_test[i].getDescriptor(nb_block));
    }
}

void Control::classification(std::string csv_train, std::string csv_test,
			     std::string descriptor_train,
			     std::string descriptor_test, std::string res_folder)
{
    vector<Channel> images_train;
    vector<int> types_train;
    vector<string> names_train;
    vector<Channel> images_test;
    vector<int> types_test;
    vector<string> names_test;
    // Read in the data. This can fail if no valid
    // input filename is given.
    try {
	read_csv(csv_train, images_train, types_train, names_train);
	read_csv(csv_test, images_test, types_test, names_test);
    } catch (cv::Exception& e) {
        cerr << "Error opening file \""
	     << csv_train <<"or "  <<csv_test
	     << "\". Reason: " << e.msg << endl;
        // nothing more we can do
        exit(1);
    }
    // Quit if there are not enough images for this demo.
    if(images_train.size() <= 1 || images_test.size() <= 1) {
        string error_message = "This demo needs at least 2 images to work. Please add more images to your data set!";
        CV_Error(CV_StsError, error_message);
    }

    match(images_train, types_train, names_train);
    match(images_test, types_test, names_test);
    IO train_io(false, strdup(descriptor_train.c_str()));
    IO test_io(false, strdup(descriptor_test.c_str()));
    std::vector< std::vector<int> > trains_v = train_io.getDescriptors();
    std::vector< std::vector<int> > tests_v = test_io.getDescriptors();    
    int N = images_test.size();
    if(N != (int)tests_v.size())
	return;
    int nb_true = 0;
    ofstream ofs(res_folder + "/result.OUT");
    for (int i = 0; i < N; ++i)
    {
//        this->predictKNNC(images_test[i], images_train, tests_v[i], trains_v);
	this->predictNNC(images_test[i], images_train, tests_v[i], trains_v);
        ofs<<"Image: " <<images_test[i].getName() <<" type: "
	   <<images_test[i].getType() <<" predict: "
	   <<images_test[i].getPredict() <<std::endl;
	if(images_test[i].getType() == images_test[i].getPredict())
	{
	    nb_true++;
	}
	else
	{
	    std::ostringstream oss;
	    oss<<images_test[i].getType();
	    std::string name = res_folder + "/" + oss.str();
	    oss.str("");
	    oss<<images_test[i].getPredict();
	    name += "_" + oss.str() + "_" + images_test[i].getName();
	    imwrite(name, images_test[i]);

	}
    }
    
    if(N != 0)
    {
	double percent = (double)nb_true/(double)N;
	std::cout<<percent*100.0 <<"%" <<std::endl;
    }
}

template <typename T>
T getMostFreq(std::vector<T> v)
{
    T mostFreqElement = 0;
    int mostFreq = 0;
    int N = v.size();
    for (int i = 0; i < N; ++i)
    {
	int freq = std::count(v.begin(), v.end(), v[i]);
	if(freq > mostFreq)
	{
	    mostFreq = freq;
	    mostFreqElement = v[i];
	}
    }
    return mostFreqElement;
}

/*
  type1_list: datas trainning of type 1
  type2_list: datas trainning of type 2
  image: test image
  This function return the type code of the type correct
  1:type1, 2:type2 and 3 is type code of other
*/
void Control::predictNNC(Channel &image, const std::vector<Channel> &train_images,
			 std::vector<int> v,
			 const std::vector< std::vector<int> > &trains_v)
{
    int N = trains_v.size();
    std::vector<double> distance_v;
    double D, minD = FLT_MAX;
    int index;
    for (int i = 0; i < N; ++i)
    {
	D = this->descriptorsDiff(v, trains_v[i]);
//	D = cosinDistance(v, trains_v[i]);
	if(minD > D)
	{
	    minD = D;
	    index = i;
	}
    }
    int type = train_images[index].getType();
    image.setPredict(type);
}

/*
  type1_list: datas trainning of type 1
  type2_list: datas trainning of type 2
  image: test image
  This function return the type code of the type correct
  1:type1, 2:type2 and 3 is type code of other
*/
void Control::predictKNNC(Channel &image, const std::vector<Channel> &train_images,
			 std::vector<int> v,
			 const std::vector< std::vector<int> > &trains_v)
{
    int K = 4;
    int N = trains_v.size();
    std::vector<Distance> distance_v;
    Distance D;
    for (int i = 0; i < N; ++i)
    {
	D.index = i;
	D.value = this->descriptorsDiff(v, trains_v[i]);
	distance_v.push_back(D);
    }
    std::sort(distance_v.begin(), distance_v.end(), funcCompare);
    std::vector<int> types;
    if(K > (int)distance_v.size())
	return;
    for (int i = 0; i < K; ++i)
    {
	int index = distance_v[i].index;
	int type = train_images[index].getType();
        types.push_back(type);
    }
    int type = getMostFreq(types);
    image.setPredict(type);
}

double Control::descriptorsDiff(const std::vector<int> &v1, const std::vector<int> &v2)
{
    double square = 0.0;
    int N = v1.size();
    for (int i = 0; i < N; ++i)
    {
	square += (v1[i] - v2[i])*(v1[i] - v2[i]);
	
    }
    return sqrt(square);
    //return square;
}

double mulVector(const std::vector<int> &v1, const std::vector<int> &v2)
{
    double sum = 0.0;
    int N = v1.size();
    if(N != (int)v2.size())
	return 0.0;
    for (int i = 0; i < N; ++i)
    {
	sum += v1[i]*v2[i];
    }
    return sum;
}

double lenghtVector(const std::vector<int> &v)
{
    double sum = 0.0;
    int N = v.size();
    for (int i = 0; i < N; ++i)
    {
	sum += v[i]*v[i];
    }
    return sqrt(sum);
}

double Control::cosinDistance(const std::vector<int> &v1, const std::vector<int> &v2)
{
    double mul = mulVector(v1, v2);
    double len1 = lenghtVector(v1);
    double len2 = lenghtVector(v2);
    double res = 1 - mul/(len1*len2);
    return res;
}


bool funcCompare(Distance d1, Distance d2)
{
    return (d1.value <= d2.value);
}
